
#region Imported Namespaces
using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Data;
using System.Drawing;
using System.Text.RegularExpressions;
using System.Web.UI;
using System.Web.UI.Design;
using System.Web.UI.WebControls;
#endregion

namespace PHSRAG.WebControls
{
	#region Class AlphaPicker
	/// <summary>
	/// The AlphaPicker displays the 26 alphabets of the English language allowing the user to select one of them.
	/// Such selections raise an event which may be handled by interested clients and do whatever is appropriate in
	/// their context.
	/// </summary>
	/// <remarks>
	/// <p>By default, all 24 alphabets are individually selectable. However, there might be cases when one or more of
	/// them needs to be disabled. For example, if the AlphaPicker is used in the context of filtering a list of
	/// user names, and if the complete list of users do not have any names begining with, say, 'Z', there would be
	/// a need to disable the selection of that alphabet.</p>
	/// <p>Rather than allowing each alphabet to be enabled/disable one-by-one, the AlphaPicker accepts, via the
	/// property <b>DataSource</b> a DataTable containing the column of, say, names which should be used to automatically
	/// control the enabling/disabling of alphabets. The property <b>DataColumn</b> is used in conjunction with the
	/// <b>DataSource</b> property to specify the name of the column used for such a feature.</p>
	/// <p>If <b>DataSource</b> is specified but <b>DataColumn</b> is not, the AlphaPicker will enable all alphabets.</p>
	/// <p>The <b>View All</b> button serves the purpose of selecting all alphabets. However, this is indicated NOT
	/// by highlighting all alphabets, but rather by hightlighting none of the alphabets but highlighting the
	/// <b>View all</b> button itself.</p>
	/// <p>Note that the entire selection behavior is implemented on client side to avoid round trips. When the containing
	/// entity wants to know the selected alphabet (if any), a call should be made to the property <b>Selection</b>.
	/// This property returns the string representation of the selected alphabet, or an empty string if no alphabet has
	/// been selected.</p>
	/// </remarks>
	/// <example>
	/// <p>The following example shows how the page would use this control. The registration of the control can be done
	/// simply by dropping the control on the page in design mode.</p>
	/// <code>
	/// <![CDATA[
	/// <%@ Register TagPrefix="cc1" Namespace="PHSRAG.WebControls" Assembly="PHSRAG.WebControls" %>
	/// ...
	/// <body>
	/// ...
	/// <cc1:AlphaPicker id="alphaPicker" runat="server" />
	/// ...
	/// </body>
	/// ]]>
	/// </code>
	/// </example>
	[ToolboxData("<{0}:AlphaPicker runat=server />"), Designer(typeof(AlphaPickerDesigner))]
	public class AlphaPicker : System.Web.UI.WebControls.WebControl, INamingContainer
	{
		#region Constants
		private const string AlphaChangedCommand = "alphaChanged";
		private const string AlphabetFilterKey = "alphabetFilter_570B04EA-A805-413e-8FB9-C1EEF33E17A7";
		private const string AlphabetResetKey = "alphabetReset_570B04EA-A805-413e-8FB9-C1EEF33E17A7";
		private const string ClientScriptKey = "clientScript_570B04EA-A805-413e-8FB9-C1EEF33E17A7";
		private const string DataColumnKey = "dataColumn_570B04EA-A805-413e-8FB9-C1EEF33E17A7";
		private const string DataSourceKey = "dataSource_570B04EA-A805-413e-8FB9-C1EEF33E17A7";
		private const string HiddenFieldID = "hiddenAlphaSelection";
		private const string DeselectAllAlphaButtons = "DeselectAllAlphaButtons";
		private const string JavascriptViewAllCallbackKey = "javascriptViewAllCallback_570B04EA-A805-413e-8FB9-C1EEF33E17A7";
		private const string SelectedAlphabetKey = "selectedAlphabet_570B04EA-A805-413e-8FB9-C1EEF33E17A7";
		private const string ViewAllCommand = "viewAll";
		#endregion

		#region Properties
		/// <summary>
		/// Get the key used to cache the object state.
		/// </summary>
		public static string CacheKey { get { return SelectedAlphabetKey; } }

		/// <summary>
		/// Get/Set the data column within the data source for enabling alphabets.
		/// </summary>
		[Bindable(true), Category("Behavior"),
		Description("Get/Set the data column within the data source for enabling alphabets")] 
		public string DataColumn
		{
			get { object o = Page.Session[DataColumnKey]; return (o == null) ? null : (string)o; }
			set { Page.Session[DataColumnKey] = value; }
		}
		
		/// <summary>
		/// Get/Set the data source for enabling alphabets.
		/// </summary>
		[Bindable(true), Category("Behavior"),
		Description("Get/Set the data source for enabling alphabets")] 
		public DataTable DataSource
		{
			get { object o = Page.Session[DataSourceKey]; return (o == null) ? null : (DataTable)o; }
			set { Page.Session[DataSourceKey] = value; }
		}

		/// <summary>
		/// Get the name of the javascript function which may be invoked to reset the state of the AlphaPicker.
		/// </summary>
		[Bindable(true), Category("Behavior"),
		Description("Get the name of the javascript function which may be invoked to reset the state of the AlphaPicker")] 
		public static string JavascriptResetFunction
		{
			get { return DeselectAllAlphaButtons; }
		}

		/// <summary>
		/// Get/Set the clientside javascript function name which should be invoked when the "View All" button is clicked.
		/// </summary>
		/// <remarks>
		/// This mechanism allows the containing entity to clear its own search state.
		/// </remarks>
		[Bindable(true), Category("Behavior"),
		Description("Get/Set the clientside javascript function name which should be invoked when the View All button is clicked")] 
		public string JavascriptViewAllCallback
		{
			get { return ViewState[JavascriptViewAllCallbackKey] as string; }
			set { ViewState[JavascriptViewAllCallbackKey] = value; }
		}

		/// <summary>
		/// Get/Set the selected alphabet. If none is selected or "View All" is selected, an emtpy string is returned.
		/// </summary>
		[Bindable(true), Category("Behavior"),
		Description("Get the selected alphabet. If none is selected, an emtpy string is returned")] 
		public string Selection
		{
			get	{ string selection = GetSelection(); return (selection == ViewAllCommand) ? string.Empty : selection; }
			set { Page.Session[SelectedAlphabetKey] =  value; }
		}
		#endregion

		#region Private Properties
		private bool IsReset
		{
			get { object o = Page.Session[AlphabetResetKey]; return o == null ? false : (bool)o; }
			set { Page.Session[AlphabetResetKey] = value; }
		}
		#endregion

		#region Events
		/// <summary>
		/// Add/Remove handler for the button click.
		/// </summary>
		[Category("Action"), Description("Raised when a button is clicked")]
		public event CommandEventHandler AlphaChanged
		{
			add		{ Events.AddHandler(AlphaChangedCommand, value); }
			remove	{ Events.RemoveHandler(AlphaChangedCommand, value); }
		}
		#endregion

		#region Public Methods
		/// <summary>
		/// Reset the state of the AlphaPicker. This simply involves removing the selection from the cache and registering
		/// the fact that a reset is requested (this information is used by the GetSelection() method).
		/// </summary>
		public void Reset()
		{
			Page.Session.Remove(SelectedAlphabetKey);
			IsReset = true;
		}
		#endregion

		#region Overrides
		/// <summary>
		/// Create all controls that will be on the Header. Notice that the creation is more than just instantiating
		/// the controls. It also involves in generating the underlying HTML so that the controls shows up per the need.
		/// </summary>
		protected override void CreateChildControls()
		{
			CreateControls();
			GenerateControlTree();
		}

		/// <summary>
		/// Invoked when an event is raised by a child control.
		/// </summary>
		/// <param name="source">Reference to the child control that raised the event</param>
		/// <param name="e">Argument associated with the event</param>
		/// <returns>True if the event was handle by this method; false otherwise</returns>
		protected override bool OnBubbleEvent(object source, EventArgs e)
		{
			bool handled = false;
            LinkButton button = source as LinkButton;
			if ((e is CommandEventArgs) && button != null)
			{
				CommandEventHandler handler = (CommandEventHandler)Events[AlphaChangedCommand];
				if (handler != null)
				{
					handler(this, new CommandEventArgs(button.CommandName, null));
					handled = true;
				}
			}
			return handled;
		}

		/// <summary>
		/// <p>If the DataSource and DataColumn properties are set, enable only those alphabets which begin the specified
		/// column in the specified data source. Otherwise, all alphabets are enabled.</p>
		/// <p>Register client-side scripts to manage the selections, and a hidden field to store the selection.</p>
		/// </summary>
		/// <param name="e">Argument associated with the event</param>
		protected override void OnPreRender(EventArgs e)
		{
			base.OnPreRender(e);

			DataTable dataSource = DataSource;
			string dataColumn = DataColumn;
			if ((dataSource != null) && (dataColumn != null) && dataSource.Columns.Contains(dataColumn))
			{
				for (char alphabet = 'A'; alphabet <= 'Z'; alphabet++)
				{
					LinkButton button = FindControl(alphabet.ToString()) as LinkButton;
					if (button != null)
						button.Enabled = (dataSource.Select(string.Format("{0} LIKE '{1}%'", dataColumn, alphabet)).Length > 0);
				}
			}

			RegisterClientScripts();

			// The hidden field is updated in two contexts:
			// 1. Client-side javascript updates the hidden field as and when the user clicks on an alphabet (no postback).
			// 2. This method updates the hidden field to persist selection across postback.
			// In the first case, the javascript sets the hidden field value to the ID of the alphabet button clicked.
			// This ID is actually the Client ID of the control which we created on server side. So, even in the second
			// case, we have to take care to set the hidden field value to the Client ID of the selected alphabet so that
			// the end result in both cases is the same.

			string selection = Selection;
			if (!String.IsNullOrEmpty(selection))
			{
				LinkButton button = FindControl(selection) as LinkButton;
				if (button != null)
					selection = button.ClientID;
			}
			Helper.RegisterHiddenFieldWithId(Page, HiddenFieldID, selection);
			IsReset = false;
		}

		#endregion

		#region Private Methods
		/// <summary>
		/// Create all controls in the AlphaPicker.
		/// </summary>
		private void CreateControls()
		{
			Controls.Clear();
		}

		/// <summary>
		/// Setup the control tree to reflect the actual (and exact) layout of the controls within the AlphaPicker.
		/// </summary>
		/// <remarks>
		/// The setup involves the following:
		/// <list type="bullet">
		/// <item><description>Create the link button for the "View All" option, and associate javascript handler</description></item>
		/// <item><description>Create link buttons for each alphabet, and associate javascript handler</description></item>
		/// </list>
		/// The javascript handler simply sets the CSS class for the selected link as appropriate.
		/// </remarks>
		private void GenerateControlTree()
		{
			Controls.Add(new LiteralControl(
				"<table class=\"AlphaPicker\">\n" +
				"\t<tr><td>\n"));

			string postOnChange = (Events[AlphaChangedCommand] == null) ? "false" : "true";

			LinkButton button = new LinkButton();
			button.CssClass = "AlphaViewAllButton";
			button.CommandName = string.Empty;
			button.ID = ViewAllCommand;
			button.Text = "View All";
			button.Attributes["onclick"] = string.Format("ResetAlphabetSelection(this); return {0};", postOnChange);
			Controls.Add(button);
			Controls.Add(new LiteralControl("</td><td>&nbsp;</td>"));

			for (char alphabet = 'A'; alphabet <= 'Z'; alphabet++)
			{
				Controls.Add(new LiteralControl("<td>"));
				button = new LinkButton();
				button.CssClass = "AlphaButton";
				button.CommandName = button.Text = button.ID = alphabet.ToString();

				button.Attributes["onclick"] = string.Format("SelectAlphabet(this, 'AlphaSelected'); return {0};", postOnChange);

				Controls.Add(button);
				Controls.Add(new LiteralControl("</td>"));
			}

			Controls.Add(new LiteralControl(
				"\t</tr>\n" +
				"</table>"));
		}

		/// <summary>
		/// Get the selected alphabet.
		/// On postback, the selected alphabet is retrieved from the Form on the page itself. The client-side javascript
		/// handler which manages the alphabet selection stores the selection in the hidden field. The format of that
		/// selection is actually the client ID of the alphabet button which was clicked upon. We extract the actual
		/// alphabet out of that ID. This selection is stored in the session prior to returning it.
		/// On non-postback, the selection is retrieved from the session. This allows us to restore the state of the
		/// selection on multiple loads of the AlphaPicker within the same session.
		/// If a reset has been requested, the method returns an emtpy string, mimicking a click on the ViewAll button.
		/// </summary>
		/// <returns>Current selection or an empty string if no selection has been made</returns>
		private string GetSelection()
		{
			string selection = string.Empty;
			if (!IsReset)
			{
				if (Page.IsPostBack)
				{
					Regex regex = new Regex(string.Format(@"{0}_(?<ID>\w+)", ClientID), RegexOptions.Compiled);
					string input = Page.Request.Form[HiddenFieldID];
					if (input != null)
					{
						Match match = regex.Match(input);
						if (match.Success)
							Page.Session[SelectedAlphabetKey] = selection = match.Groups["ID"].Value;
					}
				}
				else
				{
					object o = Page.Session[SelectedAlphabetKey];
					if (o != null)
						selection = (string)o;
				}
			}
			return selection;
		}

		/// <summary>
		/// <p>Register a client-side javascript to handle clicking the "View All" button and the alphabet buttons.
		/// When clicked, all buttons are reset to their normal state first, and then the selected button is rendered
		/// in bold and slightly larger font along with a change in its background color.</p>
		/// <p>Register a client-side javascript to set the CSS class for the currently selected button.</p>
		/// </summary>
		private void RegisterClientScripts()
		{
            if (!Page.ClientScript.IsClientScriptBlockRegistered(ClientScriptKey))
			{
				string javascriptResetFunction = JavascriptViewAllCallback;
				if (javascriptResetFunction != null)
					javascriptResetFunction = string.Format("{0}();", javascriptResetFunction);
				else
					javascriptResetFunction = string.Empty;

                Page.ClientScript.RegisterClientScriptBlock(GetType(), ClientScriptKey, string.Format(
					"<script language=\"javascript\">\n" +
					"function DeselectAllAlphaButtons()\n" +
					"{{\n" +
					"\tvar button = document.getElementById('{0}_{1}');\n" +
					"\tif (button != null)\n" +
					"\t\tbutton.className = 'AlphaViewAllButton';\n" +
					"\tfor (var a = 'A'.charCodeAt(0), z = 'Z'.charCodeAt(0); a <= z; a++)\n" +
					"\t{{\n" +
					"\t\tvar button = document.getElementById('{0}_' + String.fromCharCode(a));\n" +
					"\t\tif (button != null)\n" +
					"\t\t\tbutton.className = 'AlphaButton';\n" +
					"\t}}\n" +
					"\tvar hiddenField = document.getElementById('{3}');\n" +
					"\thiddenField.value = '';\n" +
					"}}\n\n" +
					"function SelectAlphabet(button, className)\n" +
					"{{\n" +
					"\t{2}();\n" +
					"\tbutton.className = className;\n" +
					"\tvar hiddenField = document.getElementById('{3}');\n" +
					"\thiddenField.value = button.id;\n" +
					"\t{4}\n" +
					"}}\n\n" +
					"function ResetAlphabetSelection(button)\n" +
					"{{\n" +
					"\tDeselectAllAlphaButtons();\n" +
					"\tbutton.className = 'AlphaViewAllSelected';\n" +
					"\t{4}\n" +
					"}}\n\n" +
					"</script>\n", ClientID, ViewAllCommand, DeselectAllAlphaButtons, HiddenFieldID, javascriptResetFunction));
			}

			string selection = GetSelection();
			if (!String.IsNullOrEmpty(selection))
                Page.ClientScript.RegisterStartupScript(GetType(), ClientScriptKey, string.Format(
					"<script language=\"javascript\">\n" +
					"var button = document.getElementById('{0}_{1}');\n" +
					"if (button != null)\n" +
					"\tbutton.className = '{2}';\n" +
					"</script>", ClientID, selection, selection == ViewAllCommand ? "AlphaViewAllSelected" : "AlphaSelected"));
		}
		#endregion

		#region Internal Methods
		/// <summary>
		/// This method is ONLY invoked by the associated designer to show the control contents at design time.
		/// </summary>
		internal void Design()
		{
			EnsureChildControls();
		}
		#endregion
	}
	#endregion

	#region Class AlphaPickerDesigner
	/// <summary>
	/// Designer associated with the AlphaPicker control.
	/// </summary>
	/// <remarks>
	/// This class cannot be used directly.
	/// </remarks>
	public class AlphaPickerDesigner : ControlDesigner
	{
		#region Overrides
		/// <summary>
		/// Force the AlphaPicker control to create its child controls for display at design time.
		/// </summary>
		/// <returns>HTML representation of the AlphaPicker control</returns>
		public override string GetDesignTimeHtml()
		{
			((AlphaPicker)Component).Design();
			return base.GetDesignTimeHtml();
		}
		#endregion
	}
	#endregion
}

// --- EOF ---
